//

// Copyright (c) 2020 Gobierno de España
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// SPDX-License-Identifier: MPL-2.0
//

import UIKit

/// Class packages information used to display UI blocker.
public final class ActivityData {
    /// Size of activity indicator view.
    let size: CGSize
    
    /// Message displayed under activity indicator view.
    let message: String?
    
    /// Font of message displayed under activity indicator view.
    let messageFont: UIFont
    
    /// Message spacing to activity indicator view.
    let messageSpacing: CGFloat
    
    /// Animation type.
    let type: NVActivityIndicatorType
    
    /// Color of activity indicator view.
    let color: UIColor
    
    /// Color of text.
    let textColor: UIColor
    
    /// Padding of activity indicator view.
    let padding: CGFloat
    
    /// Display time threshold to actually display UI blocker.
    let displayTimeThreshold: Int
    
    /// Minimum display time of UI blocker.
    let minimumDisplayTime: Int
    
    /// Background color of the UI blocker
    let backgroundColor: UIColor
    
    /**
     Create information package used to display UI blocker.
     
     Appropriate NVActivityIndicatorView.DEFAULT_* values are used for omitted params.
     
     - parameter size:                 size of activity indicator view.
     - parameter message:              message displayed under activity indicator view.
     - parameter messageFont:          font of message displayed under activity indicator view.
     - parameter messageSpacing:       message spacing to activity indicator view.
     - parameter type:                 animation type.
     - parameter color:                color of activity indicator view.
     - parameter padding:              padding of activity indicator view.
     - parameter displayTimeThreshold: display time threshold to actually display UI blocker.
     - parameter minimumDisplayTime:   minimum display time of UI blocker.
     - parameter textColor:            color of the text below the activity indicator view. Will match color parameter
     if not set, otherwise DEFAULT_TEXT_COLOR if color is not set.
     
     - returns: The information package used to display UI blocker.
     */
    public init(size: CGSize? = nil,
                message: String? = nil,
                messageFont: UIFont? = nil,
                messageSpacing: CGFloat? = nil,
                type: NVActivityIndicatorType? = nil,
                color: UIColor? = nil,
                padding: CGFloat? = nil,
                displayTimeThreshold: Int? = nil,
                minimumDisplayTime: Int? = nil,
                backgroundColor: UIColor? = nil,
                textColor: UIColor? = nil) {
        self.size = size ?? NVActivityIndicatorView.DEFAULT_BLOCKER_SIZE
        self.message = message ?? NVActivityIndicatorView.DEFAULT_BLOCKER_MESSAGE
        self.messageFont = messageFont ?? NVActivityIndicatorView.DEFAULT_BLOCKER_MESSAGE_FONT
        self.messageSpacing = messageSpacing ?? NVActivityIndicatorView.DEFAULT_BLOCKER_MESSAGE_SPACING
        self.type = type ?? NVActivityIndicatorView.DEFAULT_TYPE
        self.color = color ?? NVActivityIndicatorView.DEFAULT_COLOR
        self.padding = padding ?? NVActivityIndicatorView.DEFAULT_PADDING
        self.displayTimeThreshold = displayTimeThreshold ??
            NVActivityIndicatorView.DEFAULT_BLOCKER_DISPLAY_TIME_THRESHOLD
        self.minimumDisplayTime = minimumDisplayTime ?? NVActivityIndicatorView.DEFAULT_BLOCKER_MINIMUM_DISPLAY_TIME
        self.backgroundColor = backgroundColor ?? NVActivityIndicatorView.DEFAULT_BLOCKER_BACKGROUND_COLOR
        self.textColor = textColor ?? color ?? NVActivityIndicatorView.DEFAULT_TEXT_COLOR
    }
}

private protocol NVActivityIndicatorPresenterState {
    func startAnimating(presenter: NVActivityIndicatorPresenter, _ fadeInAnimation: FadeInAnimation?)
    func stopAnimating(presenter: NVActivityIndicatorPresenter, _ fadeOutAnimation: FadeOutAnimation?)
}

private struct NVActivityIndicatorPresenterStateWaitingToStart: NVActivityIndicatorPresenterState {
    func startAnimating(presenter: NVActivityIndicatorPresenter, _ fadeInAnimation: FadeInAnimation?) {
        guard let activityData = presenter.data else { return }
        
        presenter.show(with: activityData, fadeInAnimation)
        presenter.state = .animating
        presenter.waitingToStartGroup.leave()
    }
    
    func stopAnimating(presenter: NVActivityIndicatorPresenter, _ fadeOutAnimation: FadeOutAnimation?) {
        presenter.state = .stopped
        presenter.waitingToStartGroup.leave()
    }
}

private struct NVActivityIndicatorPresenterStateAnimating: NVActivityIndicatorPresenterState {
    func startAnimating(presenter: NVActivityIndicatorPresenter, _ fadeInAnimation: FadeInAnimation?) {
        // Do nothing
    }
    
    func stopAnimating(presenter: NVActivityIndicatorPresenter, _ fadeOutAnimation: FadeOutAnimation?) {
        guard let activityData = presenter.data else { return }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(activityData.minimumDisplayTime)) {
            guard presenter.state == .waitingToStop else { return }
            
            presenter.stopAnimating(fadeOutAnimation)
        }
        presenter.state = .waitingToStop
    }
}

private struct NVActivityIndicatorPresenterStateWaitingToStop: NVActivityIndicatorPresenterState {
    func startAnimating(presenter: NVActivityIndicatorPresenter, _ fadeInAnimation: FadeInAnimation?) {
        presenter.stopAnimating(nil)
        
        guard let activityData = presenter.data else { return }
        presenter.startAnimating(activityData, fadeInAnimation)
    }
    
    func stopAnimating(presenter: NVActivityIndicatorPresenter, _ fadeOutAnimation: FadeOutAnimation?) {
        presenter.hide(fadeOutAnimation)
        presenter.state = .stopped
    }
}

private struct NVActivityIndicatorPresenterStateStopped: NVActivityIndicatorPresenterState {
    func startAnimating(presenter: NVActivityIndicatorPresenter, _ fadeInAnimation: FadeInAnimation?) {
        guard let activityData = presenter.data else { return }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(activityData.displayTimeThreshold)) {
            guard presenter.state == .waitingToStart else { return }
            
            presenter.startAnimating(activityData, fadeInAnimation)
        }
        presenter.state = .waitingToStart
        presenter.waitingToStartGroup.enter()
    }
    
    func stopAnimating(presenter: NVActivityIndicatorPresenter, _ fadeOutAnimation: FadeOutAnimation?) {
        // Do nothing
    }
}

/// Presenter that displays NVActivityIndicatorView as UI blocker.
public final class NVActivityIndicatorPresenter {
    fileprivate enum State: NVActivityIndicatorPresenterState {
        case waitingToStart
        case animating
        case waitingToStop
        case stopped
        
        var performer: NVActivityIndicatorPresenterState {
            switch self {
            case .waitingToStart: return NVActivityIndicatorPresenterStateWaitingToStart()
            case .animating: return NVActivityIndicatorPresenterStateAnimating()
            case .waitingToStop: return NVActivityIndicatorPresenterStateWaitingToStop()
            case .stopped: return NVActivityIndicatorPresenterStateStopped()
            }
        }
        
        func startAnimating(presenter: NVActivityIndicatorPresenter, _ fadeInAnimation: FadeInAnimation?) {
            performer.startAnimating(presenter: presenter, fadeInAnimation)
        }
        
        func stopAnimating(presenter: NVActivityIndicatorPresenter, _ fadeOutAnimation: FadeOutAnimation?) {
            performer.stopAnimating(presenter: presenter, fadeOutAnimation)
        }
    }
    
    private let restorationIdentifier = "NVActivityIndicatorViewContainer"
    private let messageLabel: UILabel = {
        let label = UILabel()
        
        label.textAlignment = .center
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        
        return label
    }()
    
    fileprivate var state: State = .stopped
    fileprivate var data: ActivityData? // Shared activity data across states
    
    /// The group to synchronize the message so that the one set by
    /// `setMessage` is always displayed after the initial message passed to `startAnimating(_:)`.
    fileprivate let waitingToStartGroup = DispatchGroup()
    
    /// Shared instance of `NVActivityIndicatorPresenter`.
    public static let sharedInstance = NVActivityIndicatorPresenter()
    
    /// Current status of animation, read-only.
    public var isAnimating: Bool { return state == .animating || state == .waitingToStop }
    
    private init() {}
    
    // MARK: - Public interface
    
    /**
     Display UI blocker.
     
     - parameter data: Information package used to display UI blocker.
     - parameter fadeInAnimation: Fade in animation.
     */
    public final func startAnimating(_ data: ActivityData, _ fadeInAnimation: FadeInAnimation? = nil) {
        self.data = data
        state.startAnimating(presenter: self, fadeInAnimation)
    }
    
    /**
     Remove UI blocker.
     
     - parameter fadeOutAnimation: Fade out animation.
     */
    public final func stopAnimating(_ fadeOutAnimation: FadeOutAnimation? = nil) {
        state.stopAnimating(presenter: self, fadeOutAnimation)
    }
    
    /// Set message displayed under activity indicator view.
    ///
    /// - Parameter message: message displayed under activity indicator view.
    public final func setMessage(_ message: String?) {
        waitingToStartGroup.notify(queue: DispatchQueue.main) {
            self.messageLabel.text = message
        }
    }
    
    // MARK: - Helpers
    
    fileprivate func show(
        with activityData: ActivityData,
        _ fadeInAnimation: FadeInAnimation?
    ) {
        let containerView = UIView(frame: UIScreen.main.bounds)
        
        containerView.backgroundColor = activityData.backgroundColor
        containerView.restorationIdentifier = restorationIdentifier
        containerView.translatesAutoresizingMaskIntoConstraints = false
        fadeInAnimation?(containerView)
        
        let activityIndicatorView = NVActivityIndicatorView(
            frame: CGRect(x: 0, y: 0, width: activityData.size.width, height: activityData.size.height),
            type: activityData.type,
            color: activityData.color,
            padding: activityData.padding)
        
        activityIndicatorView.startAnimating()
        activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(activityIndicatorView)
        
        // Add constraints for `activityIndicatorView`.
        ({
            let xConstraint = NSLayoutConstraint(
                item: containerView,
                attribute: .centerX,
                relatedBy: .equal,
                toItem: activityIndicatorView,
                attribute: .centerX,
                multiplier: 1,
                constant: 0
            )
            let yConstraint = NSLayoutConstraint(
                item: containerView,
                attribute: .centerY,
                relatedBy: .equal,
                toItem: activityIndicatorView,
                attribute: .centerY,
                multiplier: 1,
                constant: 0
            )
            
            containerView.addConstraints([xConstraint, yConstraint])
        }())
        
        messageLabel.font = activityData.messageFont
        messageLabel.textColor = activityData.textColor
        messageLabel.text = activityData.message
        containerView.addSubview(messageLabel)
        
        // Add constraints for `messageLabel`.
        ({
            let leadingConstraint = NSLayoutConstraint(
                item: containerView,
                attribute: .leading,
                relatedBy: .equal,
                toItem: messageLabel,
                attribute: .leading,
                multiplier: 1,
                constant: -8
            )
            let trailingConstraint = NSLayoutConstraint(
                item: containerView,
                attribute: .trailing,
                relatedBy: .equal,
                toItem: messageLabel,
                attribute: .trailing,
                multiplier: 1,
                constant: 8
            )
            
            containerView.addConstraints([leadingConstraint, trailingConstraint])
        }())
        ({
            let spacingConstraint = NSLayoutConstraint(
                item: messageLabel,
                attribute: .top,
                relatedBy: .equal,
                toItem: activityIndicatorView,
                attribute: .bottom,
                multiplier: 1,
                constant: activityData.messageSpacing
            )
            
            containerView.addConstraint(spacingConstraint)
        }())
        
        var keyWindow: UIView?
        if #available(iOS 13.0, *) {
            guard let kWindow = UIApplication.shared.connectedScenes
                    .filter({$0.activationState == .foregroundActive})
                    .map({$0 as? UIWindowScene})
                    .compactMap({$0})
                    .first?.windows
                    .filter({$0.isKeyWindow}).first else { return }
            
            keyWindow = kWindow
            
        } else {
            keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
        }
        
        if let keyWindow = keyWindow {
            keyWindow.addSubview(containerView)
            
            // Add constraints for `containerView`.
            ({
                let leadingConstraint = NSLayoutConstraint(
                    item: keyWindow,
                    attribute: .leading,
                    relatedBy: .equal,
                    toItem: containerView,
                    attribute: .leading,
                    multiplier: 1,
                    constant: 0
                )
                let trailingConstraint = NSLayoutConstraint(
                    item: keyWindow,
                    attribute: .trailing,
                    relatedBy: .equal,
                    toItem: containerView,
                    attribute: .trailing,
                    multiplier: 1,
                    constant: 0
                )
                let topConstraint = NSLayoutConstraint(
                    item: keyWindow,
                    attribute: .top,
                    relatedBy: .equal,
                    toItem: containerView,
                    attribute: .top,
                    multiplier: 1,
                    constant: 0
                )
                let bottomConstraint = NSLayoutConstraint(
                    item: keyWindow,
                    attribute: .bottom,
                    relatedBy: .equal,
                    toItem: containerView,
                    attribute: .bottom,
                    multiplier: 1,
                    constant: 0
                )
                
                keyWindow.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
            }())
        }

    }
    
    fileprivate func hide(_ fadeOutAnimation: FadeOutAnimation?) {
        for window in UIApplication.shared.windows {
            for item in window.subviews
            where item.restorationIdentifier == restorationIdentifier {
                if let fadeOutAnimation = fadeOutAnimation {
                    fadeOutAnimation(item) {
                        item.removeFromSuperview()
                    }
                } else {
                    item.removeFromSuperview()
                }
            }
        }
    }
}
